﻿using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using DependencyAnalysis.DataModel;

namespace DependencyAnalysis.Internal
{
    public class Methodenreinheit
    {
        private readonly IDependencyModel _model;

        private static readonly HashSet<string> ImpureCollectionMembers = new HashSet<string>
        {
            "System.Collections.Generic.IList.§Void Insert(Int32, *)", 
            "System.Collections.Generic.IList.§Void set_Item(Int32, *)",
            "System.Collections.Generic.IList.§Void RemoveAt(Int32)",
            "System.Collections.Generic.ICollection.§Void Add(*)",
            "System.Collections.Generic.ICollection.§Void Clear(*)",
            "System.Collections.Generic.ICollection.§Boolean Remove(*)",
            "System.Collections.IList.§Void Insert(Int32, *)", 
            "System.Collections.IList.§Void set_Item(Int32, *)",
            "System.Collections.IList.§Void RemoveAt(Int32)",
            "System.Collections.IList.§Void Add(*)",
            "System.Collections.IList.§Void Clear(*)",
            "System.Collections.IList.§Void Remove(*)",
            "System.Collections.Stack.§Void Clear()",
            "System.Collections.Stack.§* Pop()",
            "System.Collections.Stack.§Void Push(*)",
            "System.Collections.Generic.Stack.§Void Clear()",
            "System.Collections.Generic.Stack.§* Pop()",
            "System.Collections.Generic.Stack.§Void Push(*)",
            "System.Collections.Queue.§Void Clear()",
            "System.Collections.Queue.§* Dequeue()",
            "System.Collections.Queue.§Void Enqueue(*)",
            "System.Collections.Generic.Queue.§Void Clear()",
            "System.Collections.Generic.Queue.§* Dequeue()",
            "System.Collections.Generic.Queue.§Void Enqueue(*)",
            "System.Collections.LinkedList.§Void Clear()",
            "System.Collections.LinkedList.§* AddAfter(*)",
            "System.Collections.LinkedList.§* AddBefore(*)",
            "System.Collections.LinkedList.§* AddFirst(*)",
            "System.Collections.LinkedList.§* AddLast(*)",
            "System.Collections.LinkedList.§* Remove(*)",
            "System.Collections.LinkedList.§Void RemoveFirst()",
            "System.Collections.LinkedList.§Void RemoveLast()",
            "System.Collections.Generic.LinkedList.§Void Clear()",
            "System.Collections.Generic.LinkedList.§* AddAfter(*)",
            "System.Collections.Generic.LinkedList.§* AddBefore(*)",
            "System.Collections.Generic.LinkedList.§* AddFirst(*)",
            "System.Collections.Generic.LinkedList.§* AddLast(*)",
            "System.Collections.Generic.LinkedList.§* Remove(*)",
            "System.Collections.Generic.LinkedList.§Void RemoveFirst()",
            "System.Collections.Generic.LinkedList.§Void RemoveLast()",
            "System.Collections.ObjectModel.Collection.§Void Add(*)",
            "System.Collections.ObjectModel.Collection.§Void Clear()",
            "System.Collections.ObjectModel.Collection.§Boolean Remove(*)",
            "System.Collections.ObjectModel.Collection.§Void set_Item(Int32, *)",
            "System.Collections.ObjectModel.Collection.§Void RemoveAt(Int32)",
            "System.Collections.ObjectModel.Collection.§Void SetItem(Int32, *)",
            "System.Collections.ObjectModel.Collection.§Void RemoveItem(Int32)",
            "System.Collections.ObjectModel.Collection.§Void Insert(Int32, *)",
            "System.Collections.ObjectModel.Collection.§Void InsertItem(Int32, *)",
            "System.Collections.ObjectModel.Collection.§Void ClearItems()",
            "System.Collections.Generic.List.§Void Insert(Int32, *)", 
            "System.Collections.Generic.List.§Void InsertRange(Int32, *)", 
            "System.Collections.Generic.List.§Void set_Item(Int32, *)",
            "System.Collections.Generic.List.§Void RemoveAt(Int32)",
            "System.Collections.Generic.List.§Void RemoveAll(*)",
            "System.Collections.Generic.List.§Void RemoveRange(Int32, Int32)",
            "System.Collections.Generic.List.§Void Add(*)",
            "System.Collections.Generic.List.§Void AddRange(*)",
            "System.Collections.Generic.List.§Void Clear(*)",
            "System.Collections.Generic.List.§Void Remove(*)",
            "System.Collections.Generic.List.§Void Sort(*)",
            "System.Collections.Generic.List.§Void Reverse(*)",
            "System.Collections.ArrayList.§Void Insert(Int32, *)", 
            "System.Collections.ArrayList.§Void InsertRange(Int32, *)", 
            "System.Collections.ArrayList.§Void SetRange(Int32, *)", 
            "System.Collections.ArrayList.§Void set_Item(Int32, *)",
            "System.Collections.ArrayList.§Void RemoveAt(Int32)",
            "System.Collections.ArrayList.§Void RemoveRange(Int32, Int32)",
            "System.Collections.ArrayList.§Int32 Add(*)",
            "System.Collections.ArrayList.§Void AddRange(*)",
            "System.Collections.ArrayList.§Void Clear(*)",
            "System.Collections.ArrayList.§Void Remove(*)",
            "System.Collections.ArrayList.§Void Sort(*)",
            "System.Collections.ArrayList.§Void Reverse(*)",
            "System.Collections.SortedList.§Void Add(*)",
            "System.Collections.SortedList.§Void Clear()",
            "System.Collections.SortedList.§Void Remove(*)",
            "System.Collections.SortedList.§Void RemoveAt(*)",
            "System.Collections.SortedList.§Void SetByIndex(Int32, *)",
            "System.Collections.SortedList.§Void set_Item(Int32, *)",
            "System.Collections.Generic.SortedList.§Void Add(*)",
            "System.Collections.Generic.SortedList.§Void Clear()",
            "System.Collections.Generic.SortedList.§Void Remove(*)",
            "System.Collections.Generic.SortedList.§Void RemoveAt(*)",
            "System.Collections.Generic.SortedList.§Void set_Item(Int32, *)",
            "System.Collections.Generic.SortedSet.§Boolean Add(*)",
            "System.Collections.Generic.SortedSet.§Void Clear()",
            "System.Collections.Generic.SortedSet.§Boolean Remove(*)",
            "System.Collections.Generic.SortedSet.§Int32 RemoveWhere(*)",
            "System.Collections.Generic.SortedSet.§Void Reverse(*)",
            "System.Collections.Generic.SortedSet.§Void ExceptWith(*)",
            "System.Collections.Generic.SortedSet.§Void IntersectWith(*)",
            "System.Collections.Generic.SortedSet.§Void SymmetricExceptWith(*)",
            "System.Collections.Generic.SortedSet.§Void UnionWith(*)",
            "System.Collections.Generic.SortedDictionary.§Void Add(*)",
            "System.Collections.Generic.SortedDictionary.§Void Clear()",
            "System.Collections.Generic.SortedDictionary.§Boolean Remove(*)",
            "System.Collections.Concurrent.ConcurrentQueue.§Boolean TryDequeue(*)",
            "System.Collections.Concurrent.ConcurrentQueue.§Void Enqueue(*)",

            "*.§Void set_*(*)",
            "*.§Void add_*(*)",
            "*.§Void remove_*(*)"
        };

        public Methodenreinheit(IDependencyModel model)
        {
            _model = model;
        }

        private static bool ProjectMethods(IDescriptor x)
        {
            var member = (MemberDescriptor)x;
            return member.MemberType == MemberType.Method && member.ImplementingType.Submodule.Module.IsPartOfProject;
        }

        public void Evaluate()
        {
            // select all methods
            var methods = (from x in _model.DependenciesPerMemberOut.Keys
                           where ProjectMethods(x)
                           select (MemberDescriptor)x).ToArray();
            foreach (var method in methods)
            {
                method.Purity = Purity.Pure;
            }
            var nonProjectMethods = MarkNonProjectMethods(_model);

            MarkModifyCommon(methods, nonProjectMethods);

            MarkModifySelf(methods, nonProjectMethods);

            MarkModifyArgument(methods);
        }

        private void MarkModifyArgument(IEnumerable<MemberDescriptor> methods)
        {
            foreach (var currentMethod in methods)
            {
                if (currentMethod.Signature.Contains(" ByRef,") ||
                    currentMethod.Signature.Contains(" ByRef)"))
                {
                    currentMethod.Purity |= Purity.ModifyParameter;
                    continue;
                }
                var parameters = (from x in _model.DependenciesPerMemberOut[currentMethod]
                                  where x.ReferenceType == ReferenceType.Parameter
                                  select x).ToArray();
                var parameterVariants = new HashSet<TypeDescriptor>();
                bool containsArray = false;
                foreach (var parameter in parameters)
                {
                    var implementingType = parameter.Target.ImplementingType;
                    containsArray |= AddVariants(implementingType, parameterVariants, true);
                }
                var instructions = _model.InstructionCache.GetInstructions(currentMethod);
                for (int index0 = 0; index0 < instructions.Count; index0++)
                {
                    var instruction = instructions[index0];
                    if (InstructionCache.StoreArrayElementInstructions.Contains(instruction.OpCode) && containsArray)
                    {
                        for (int index1 = index0; index1 > -1; index1--)
                        {
                            var backtrackInstruction = instructions[index1];
                            var calledMember = backtrackInstruction.Operand as MemberDescriptor;
                            if (calledMember != null && calledMember.IsStatic &&
                                RepresentsArrayMember(calledMember))
                            {
                                break;
                            }
                            var operand = backtrackInstruction.Operand as TypeDescriptor;
                            if (operand != null && InstructionCache.ArgumentOpCodes.Contains(backtrackInstruction.OpCode) &&
                                parameterVariants.Contains(operand) && operand.Name.EndsWith("[]"))
                            {
                                currentMethod.Purity |= Purity.ModifyParameter;
                                break;
                            }
                        }
                    }
                    else if (InstructionCache.MethodOpCodes.Contains(instruction.OpCode))
                    {
                        var operand = (MemberDescriptor)instruction.Operand;
                        if ((operand.Purity & (Purity.ModifySelf | Purity.CallModifySelf)) != 0 &&
                            parameterVariants.Contains(operand.ImplementingType))
                        {
                            var pseudoStack = new PseudoStack { Instructions = instructions };
                            var callTarget = pseudoStack.TrackCallTarget(instruction);
                            if (InstructionCache.ArgumentOpCodes.Contains(callTarget.OpCode))
                            {
                                if (operand.IsStatic || callTarget.OpCode != OperationCode.Ldarg_0)
                                {
                                    currentMethod.Purity |= Purity.ModifyParameter;
                                }
                                break;
                            }
                        }
                    }
                }
            }
        }

        public static bool AddVariants(TypeDescriptor implementingType, HashSet<TypeDescriptor> parameterVariants, bool upwards)
        {
            parameterVariants.Add(implementingType);
            if (implementingType == null || implementingType.Name == null)
            {
                return false;
            }
            bool containsArray = implementingType.Name.EndsWith("[]");
            var nextLevel = upwards ? implementingType.InheritedTypes : implementingType.ImplementingTypes;
            if (nextLevel == null)
            {
                return containsArray;
            }
            foreach (var type in nextLevel)
            {
                containsArray |= AddVariants(type, parameterVariants, true);
            }
            return containsArray;
        }

        private void MarkModifySelf(MemberDescriptor[] methods, IList<MemberDescriptor> nonProjectMethods)
        {
            var markedMethods = new ConcurrentQueue<MemberDescriptor>();
            // check for ModifySelf
            foreach (var method in methods)
            {
                if (!method.IsStatic && ModifiesSelf(method, _model.InstructionCache))
                {
                    method.Purity |= Purity.ModifySelf;
                    if (!method.Name.StartsWith("set_"))
                    {
                        markedMethods.Enqueue(method);
                    }
                }
            }
            foreach (var method in nonProjectMethods)
            {
                if ((method.Purity & Purity.ModifySelf) == Purity.ModifySelf)
                {
                    if (!method.Name.StartsWith("set_"))
                    {
                        markedMethods.Enqueue(method);
                    }
                }
            }
            var markedMethodsCommon = new ConcurrentQueue<MemberDescriptor>();
            MemberDescriptor currentMethod;
            int numberInitial = markedMethods.Count;
            int index = 0;
            // crawl call tree within same type and deriatives, mark with CallModifySelf
            while (markedMethods.Count > 0 && markedMethods.TryDequeue(out currentMethod))
            {
                index++;
                if (index > numberInitial)
                {
                    currentMethod.Purity |= Purity.CallModifySelf;
                }
                var relevantTypes =
                    new List<TypeDescriptor>(currentMethod.ImplementingType.ImplementingTypes ?? new TypeDescriptor[] { });
                relevantTypes.Add(currentMethod.ImplementingType);
                var callers = (from x in _model.DependenciesPerMember[currentMethod]
                               where
                               (x.Source.Purity & Purity.CallModifySelf) == 0 &&
                                   (!x.Source.IsStatic &&
                                   relevantTypes.Contains(x.Source.ImplementingType))
                               select x.Source).ToList();
                var callersIndirect = (from x in _model.DependenciesPerMember[currentMethod]
                                       where
                                       (x.Source.Purity & Purity.CallModifySelf) == 0 &&
                                           (_model.DependenciesPerTypeOut[x.Source.ImplementingType].Any(y => y.ReferenceType == ReferenceType.Field))
                                       select x.Source).ToList();
                var currentMethodClosure = currentMethod;

                Parallel.ForEach(callers, caller =>
                {
                    var instructions = _model.InstructionCache.GetInstructions(caller);
                    foreach (var instruction in instructions)
                    {
                        if (InstructionCache.MethodOpCodes.Contains(instruction.OpCode) &&
                            Equals(currentMethodClosure, instruction.Operand))
                        {
                            var pseudoStack = new PseudoStack { Instructions = instructions };
                            var callTarget = pseudoStack.TrackCallTarget(instruction);
                            if (InstructionCache.ArgumentOpCodes.Contains(callTarget.OpCode))
                            {
                                if (callTarget.OpCode == OperationCode.Ldarg_0)
                                {
                                    markedMethods.Enqueue(caller);
                                    break;
                                }
                            }
                        }
                    }
                });
                Parallel.ForEach(callersIndirect, caller => MarkModifiersThroughGetters(caller, currentMethodClosure, markedMethods, markedMethodsCommon));
            }
            foreach (var member in markedMethodsCommon)
            {
                member.Purity |= Purity.CallModifyCommon;
            }
            MarkCallModifyCommon(markedMethodsCommon);
        }

        private void MarkModifiersThroughGetters(MemberDescriptor caller, MemberDescriptor currentMethodClosure,
            ConcurrentQueue<MemberDescriptor> markedMethods, ConcurrentQueue<MemberDescriptor> markedMethodsCommon)
        {
            Instruction lastStackInstrucion = null;
            var instructions = _model.InstructionCache.GetInstructions(caller);
            foreach (var instruction in instructions)
            {
                if (InstructionCache.MethodOpCodes.Contains(instruction.OpCode) &&
                    Equals(currentMethodClosure, instruction.Operand))
                {
                    if (lastStackInstrucion != null &&
                        (InstructionCache.FieldOpCodes.Contains(lastStackInstrucion.OpCode)
                         ||
                         InstructionCache.MethodOpCodes.Contains(lastStackInstrucion.OpCode)
                         && ((MemberDescriptor)lastStackInstrucion.Operand).Name.StartsWith("get_")))
                    {
                        var operand = (MemberDescriptor)lastStackInstrucion.Operand;
                        if (operand.IsStatic)
                        {
                            markedMethodsCommon.Enqueue(caller);
                        }
                        else
                        {
                            markedMethods.Enqueue(caller);
                        }
                    }
                    break;
                }
                if (InstructionCache.ArgumentOpCodes.Contains(instruction.OpCode) ||
                    InstructionCache.FieldOpCodes.Contains(instruction.OpCode) ||
                    InstructionCache.LocalOpCodes.Contains(instruction.OpCode) ||
                    InstructionCache.MethodOpCodes.Contains(instruction.OpCode))
                {
                    lastStackInstrucion = instruction;
                }
            }
        }

        private static bool RepresentsArrayMember(MemberDescriptor member)
        {
            return member.ReturningType != null &&
                   member.ReturningType.Name.EndsWith("[]") &&
                   (member.MemberType == MemberType.Field || member.Name.StartsWith("get_"));
        }

        private void MarkModifyCommon(IEnumerable<MemberDescriptor> methods, IEnumerable<MemberDescriptor> nonProjectMethods)
        {
            var markedMethods = new ConcurrentQueue<MemberDescriptor>();
            var pseudoStack = new PseudoStack();
            foreach (var method in methods)
            {
                var instructions = _model.InstructionCache.GetInstructions(method);
                for (int index0 = 0; index0 < instructions.Count; index0++)
                {
                    var instruction = instructions[index0];
                    if (InstructionCache.StoreArrayElementInstructions.Contains(instruction.OpCode))
                    {
                        pseudoStack.Instructions = instructions;
                        var backtrackInstruction = pseudoStack.TrackCallTarget(instruction);
                        var calledMember = backtrackInstruction.Operand as MemberDescriptor;
                        if (calledMember != null && calledMember.IsStatic &&
                            RepresentsArrayMember(calledMember))
                        {
                            method.Purity |= Purity.ModifyCommon;
                            markedMethods.Enqueue(method);
                            break;
                        }
                    }
                    else if (instruction.OpCode == OperationCode.Stsfld)
                    {
                        var setField = instruction.Operand as MemberDescriptor;
                        if (setField != null && !setField.Name.Contains("<>")
                            && !setField.FullName.Contains("<PrivateImplementationDetails>"))
                        {
                            method.Purity |= Purity.ModifyCommon;
                            markedMethods.Enqueue(method);
                            break;
                        }
                    }
                }
            }
            foreach (var method in nonProjectMethods)
            {
                if ((method.Purity & Purity.ModifyCommon) == Purity.ModifyCommon)
                {
                    markedMethods.Enqueue(method);
                }
            }
            MarkCallModifyCommon(markedMethods);
        }

        private void MarkCallModifyCommon(ConcurrentQueue<MemberDescriptor> markedMethods)
        {
            int numberInitial = markedMethods.Count;
            int index = 0;

            MemberDescriptor currentMethod;
            while (markedMethods.Count > 0 && markedMethods.TryDequeue(out currentMethod))
            {
                index++;
                if (index > numberInitial)
                {
                    currentMethod.Purity |= Purity.CallModifyCommon;
                }
                var callers = (from x in _model.DependenciesPerMember[currentMethod]
                               where (x.Source.Purity & Purity.CallModifyCommon) == 0
                               select x.Source).ToList();
                callers.ForEach(markedMethods.Enqueue);
            }
        }

        private bool ModifiesSelf(MemberDescriptor member, InstructionCache instructionCache)
        {
            var checkedTypes = new List<TypeDescriptor>();
            if (member.ImplementingType.InheritedTypes != null)
            {
                checkedTypes.AddRange(member.ImplementingType.InheritedTypes);
            }
            checkedTypes.Add(member.ImplementingType);
            var methodsToCheck = new[] { member };
            return MethodModifiesField(instructionCache, methodsToCheck, checkedTypes);
        }

        public static bool MethodModifiesField(InstructionCache instructionCache, IEnumerable<MemberDescriptor> ownMethodCalls, IList<TypeDescriptor> checkedTypes)
        {
            var pseudoStack = new PseudoStack();
            foreach (var methodCall in ownMethodCalls)
            {
                var instructionsOfMethod = instructionCache.GetInstructions(methodCall);
                var stack = new List<Instruction>();
                for (int index = 0; index < instructionsOfMethod.Count; index++)
                {
                    var count = stack.Count;
                    var instruction = instructionsOfMethod[index];
                    if (InstructionCache.StoreArrayElementInstructions.Contains(instruction.OpCode))
                    {
                        pseudoStack.Instructions = instructionsOfMethod;
                        var backtrackInstruction = pseudoStack.TrackCallTarget(instruction);
                        var calledMember = backtrackInstruction.Operand as MemberDescriptor;
                        if (calledMember != null && RepresentsArrayMember(calledMember))
                        {
                            var backtrackIndex = instructionsOfMethod.IndexOf(backtrackInstruction);
                            if (backtrackIndex > 0 &&
                                instructionsOfMethod[backtrackIndex - 1].OpCode == OperationCode.Ldarg_0)
                            {
                                return true;
                            }
                        }

                    }
                    else if (instruction.OpCode == OperationCode.Stfld && count > 1 &&
                             stack[count - 2].OpCode == OperationCode.Ldarg_0)
                    {
                        var memberDescriptor = (MemberDescriptor)instruction.Operand;
                        if (checkedTypes.Contains(memberDescriptor.ImplementingType))
                        {
                            return true;
                        }
                    }
                    else if (InstructionCache.MethodOpCodes.Contains(instruction.OpCode))
                    {
                        var calledMember = (MemberDescriptor)instruction.Operand;
                        if (count > 1 && calledMember.Name.StartsWith("set_") && stack[count - 2].OpCode == OperationCode.Ldarg_0)
                        {
                            return true;
                        }
                    }
                    if (InstructionCache.ArgumentOpCodes.Contains(instruction.OpCode) ||
                        InstructionCache.FieldOpCodes.Contains(instruction.OpCode) ||
                        InstructionCache.LocalOpCodes.Contains(instruction.OpCode) ||
                        instruction.OpCode == OperationCode.Newobj)
                    {
                        stack.Add(instruction);
                    }
                }
            }
            return false;
        }

        private static IList<MemberDescriptor> MarkNonProjectMethods(IDependencyModel model)
        {
            var markedMembers = new List<MemberDescriptor>();
            var patterns = (from pattern in ImpureCollectionMembers
                            select new Regex("^" + Regex.Escape(pattern).Replace(@"\*", ".*").Replace(@"\?", ".") + "$")).ToArray();
            foreach (var method in model.DependenciesPerMember.Keys.OfType<MemberDescriptor>())
            {
                var methodClosure = method;
                if (patterns.Any(x => x.IsMatch(methodClosure.FullName)))
                {
                    method.Purity = Purity.Pure;
                    if (method.IsStatic)
                    {
                        method.Purity |= Purity.ModifyCommon;
                    }
                    else
                    {
                        method.Purity |= Purity.ModifySelf;
                    }
                    markedMembers.Add(method);
                }
            }
            return markedMembers;
        }
    }
}
